맨위로가기

비동기 입출력

"오늘의AI위키"는 AI 기술로 일관성 있고 체계적인 최신 지식을 제공하는 혁신 플랫폼입니다.
"오늘의AI위키"의 AI를 통해 더욱 풍부하고 폭넓은 지식 경험을 누리세요.

1. 개요

비동기 입출력(I/O)은 I/O 요청 후 완료 여부를 즉시 확인하지 않고 콜백이나 신호 등의 별도 메커니즘을 통해 확인하는 방식을 의미한다. 블로킹/논블로킹, 동기/비동기 개념과 구분되며, 일반적으로 논블로킹 방식으로 동작하지만 항상 그런 것은 아니다. 비동기 I/O는 버퍼 처리, 입출력 성패 확인, 시스템 콜 종료 등의 특징을 가지며, 실시간 운영체제, 엔터프라이즈 환경, 이벤트 드리븐 프레임워크 등 다양한 환경에서 사용된다. 구현 방식은 프로세스 분산, 폴링, select 루프, 콜백, 스레드, 파이버, 완료 큐 등 운영체제, 프로그래밍 언어, 라이브러리에 따라 다양하다. 자바, C++, C#, 파이썬, 자바스크립트 등 여러 프로그래밍 환경에서 비동기 프로그래밍을 지원하며, 리눅스와 윈도우 등 운영체제에서도 구현된다.

2. 블로킹/논블로킹, 동기/비동기

비동기 입출력(I/O)은 논블로킹 I/O와 매우 자주 혼동되지만, 동기/비동기, 블로킹/논블로킹이라는 분류는 반드시 일치하지 않는다. POSIX 환경에서 `O_NONBLOCK`이 설정된 파일 디스크립터에 대해 일반적인 `read`나 `write`를 수행하면 논블로킹이 되지만, "블록될 것 같으면 에러로 한다"는 동작이므로 비동기가 되는 것은 아니다.[1] (대부분의 I/O 작업은 운영체제(OS) 내의 버퍼 등에 의해, 동기형 API에서도 블록되지 않고 완료될 수 있는 경우도 많다).[1]

비동기 I/O는 다음과 같은 특징을 갖는다.[1]

# 버퍼의 내용이 커널 등에 의해 복사되거나, 또는 프로그래머의 책임으로 처리가 완료될 때까지 요구하는 프로세스가 그것을 유지해야 한다.[1]

# (권한 위반 등, 즉시 커널이 에러 등으로 할 수 있는 경우를 제외하고) 입출력의 성패도, 입출력을 요구하는 시스템 콜의 결과로는 얻을 수 없으며, 콜백 또는 다른 시스템 콜 등으로 다시 얻을 필요가 있다.[1]

# 위와 같은 제한 하에, 입출력 요구의 시스템 콜은 블록되지 않고, 최소한의 처리로 즉시 종료된다.[1]

이러한 API 스타일에 의한 I/O가 비동기 I/O이다. 따라서 비동기 I/O가 이용되는 것은, "시간 제약이 엄격한 RTOS이기 때문에"와 같은 이유가 아니다.[1] 상호 배제 제어의 사정 등으로 블록시킬 수 없거나, 또는 성능상의 이유로 엔터프라이즈 용도로 요구되는 경우도 있으며, 이벤트 드리븐 형 프레임워크이기 때문에 필요하다는 경우도 있다.[1]

비동기 입출력 형태와 POSIX 함수의 예시는 다음과 같다.

블로킹논블로킹비동기
APIwrite, readwrite, read + poll / selectaio_write, aio_read



모든 형태의 비동기 입출력은 잠재적인 자원 충돌과 관련된 실패의 가능성을 열어둔다. 이를 방지하기 위해서는 상호 배제, 세마포어 등을 사용하는 주의 깊은 프로그래밍이 필요하다.

3. 구현 방식

다양한 운영체제, 프로그래밍 언어, 라이브러리에서 비동기 I/O를 구현하는 여러 방식이 존재한다. 주요 구현 방식은 다음과 같다:


  • 프로세스 분산: 초기 유닉스 시스템에서 사용된 방식으로, 여러 프로세스가 독립적으로 실행되며 각각 자체 I/O 흐름을 처리한다. 파이프라인을 통해 연결된다. 프로세스 생성 및 유지 비용이 크다는 단점이 있다. 데이터 흐름 프로그래밍으로 확장될 수 있다.
  • 폴링(Polling):
  • 논블로킹 동기 API를 제공하며, 프로세스가 반복적으로 폴링하여 CPU 시간을 낭비할 수 있다.
  • 하드웨어 I/O 병렬 처리를 완전히 활용하지 못할 수 있다.
  • 예시:
  • select: BSD 유닉스 기반 시스템에서 사용되며, `select` 시스템 호출을 사용하여 파일 디스크립터의 조건 발생, 타임아웃, 시그널 수신을 대기한다. 이벤트 루프로 구현될 수 있다.
  • poll: SVR3 유닉스에서 제공되며, `select`와 유사하다.
  • 리눅스의 `epoll`: 이벤트 발생 소스만 반환.
  • FreeBSD의 `kqueue`.
  • Solaris의 이벤트 포트.
  • 신호(Signal): BSDPOSIX 유닉스에서 사용되며, I/O 완료 시 신호(인터럽트)가 생성된다. 신호 처리기 내에서 사용 가능한 기능이 제한적이며, 데이터 구조 불일치 문제가 발생할 수 있다.
  • 콜백(Callback): 클래식 Mac OS, VMS, Windows에서 사용된다. 각 I/O 요청에 자체 완료 함수가 할당된다. 스택 깊이 문제가 발생할 수 있지만, 일반적으로 새로운 I/O 시작 시 자체 반환되어 해결된다.
  • 경량 프로세스(LWP) 또는 스레드: 각 LWP 또는 스레드가 블로킹 동기 I/O를 사용한다. 많은 스레드가 필요하여 웹 서버와 같은 대규모 애플리케이션에는 부적합하다. Erlang 런타임 시스템에서도 사용된다.
  • 파이버 / 코루틴: Erlang 런타임 시스템 외부에서 비동기 I/O를 수행하는 경량 접근 방식이다.
  • 완료 큐(Completion Queue): 마이크로소프트 윈도우, 솔라리스, AmigaOS, DNIX, 리눅스(io_uring 사용)에서 사용된다. I/O 요청은 비동기적으로 발행되고, 완료 알림은 동기화 큐를 통해 제공된다.
  • 완료 이벤트 플래그(Completion Event Flag): VMS 및 AmigaOS에서 사용되며, 깊이가 1인 완료 큐와 유사하다.
  • 채널 I/O: IBM, 불 그룹(Groupe Bull), 유니시스(Unisys) 메인프레임에서 사용된다. 보조 프로세서가 I/O 작업을 처리하여 CPU 사용률과 처리량을 최대화한다.
  • IOPS 최적화: Windows Server 2012 및 Windows 8에서 사용되며, 대량의 메시지를 처리하는 애플리케이션에 최적화되어 있다.[2]


일반적인 컴퓨팅 하드웨어는 폴링과 인터럽트, 두 가지 방법으로 비동기 I/O를 구현한다. DMA는 폴링 또는 인터럽트당 더 많은 작업을 수행하는 수단이다.

리눅스에서는 POSIX-XSI, POSIX 1003.1b, io_uring 구현이 이루어지고 있다.

윈도우에서는 Windows NT 3.1 이후 모든 버전에서 구현이 이루어지고 있다. 윈도우 API는 비동기 버전 함수를 제공하며, "I/O 완료 포트"를 통해 최적의 워커 스레드 수 제어와 I/O 오프로드를 구현할 수 있다.

4. 비동기 프로그래밍 환경

Java 1.5에서 표준화된 나 C++11에서 표준화된 `std::async`/`std::future` 등은 스레드 기반의 Future를 실현한다.[3][4] .NET Framework/.NET Core에서는 C# 등 .NET 언어에서 사용할 수 있는 Task Parallel Library|태스크 병렬 라이브러리영어 (TPL)와 Microsoft Visual C++의 동시 실행 런타임에서는 C++에서 사용할 수 있는 Parallel Patterns Library|병렬 패턴 라이브러리영어 (PPL)이 제공된다.[5]

이를 발전시켜 C# 5.0/VB.NET 11 이후, Python 3.5 이후에는 async/await 구문이 제공된다. F#에는 비동기 워크플로(asynchronous workflow)라고 불리는, TPL과는 다른 독자적인 인프라를 이용한 비동기 프로그래밍 기능이 있다. C++에서는 C++20에서 `co_await` 구문이 표준화되었다.[6]

JavaScript는 싱글 스레드로 동작하므로 비동기 프로그래밍은 이벤트 구동 기반의 유사한 방식에 의존했지만, Web Worker에 의해 멀티 스레드 프로그래밍이 지원된다. (Web Worker의 스레드는 메모리 공간을 공유하지 않고, 실제로는 메시지 기반의 멀티 프로세스 프로그래밍이다.) ECMAScript 2015 (ES2015)에서는 Promise가, ES2017에서는 async/await 구문이 표준화되었다.

5. 예제

I/O를 읽는 세 가지 접근 방식의 예시이다. 객체와 함수는 추상적이다.[1]


  • 블로킹 및 논블로킹, 동기: `IO.poll()`은 최대 5초 동안 블로킹될 수 있지만 `device.read()`는 그렇지 않다.


```python

device = IO.open()

ready = False

while not ready:

print("읽을 데이터가 없습니다!")

ready = IO.poll(device, IO.INPUT, 5) # 5초가 경과하거나 읽을 데이터가 있는 경우(INPUT) 제어를 반환한다.

data = device.read()

print(data)

```

  • '''논블로킹, 비동기'''

```python

ios = IO.IOService()

device = IO.open(ios)

def inputHandler(data, err):

"입력 데이터 처리기"

if not err:

print(data)

device.readSome(inputHandler)

ios.loop() # 모든 작업이 완료될 때까지 대기하고 모든 적절한 처리기를 호출합니다.

```
```python

ios = IO.IOService()

device = IO.open(ios)

async def task():

try:

data = await device.readSome()

print(data)

except Exception:

pass

ios.addTask(task)

ios.loop() # 모든 작업이 완료될 때까지 대기하고 모든 적절한 처리기를 호출합니다.

```

  • '''Reactor 패턴'''

```python

device = IO.open()

reactor = IO.Reactor()

def inputHandler(data):

"입력 데이터 처리기"

print(data)

reactor.stop()

reactor.addHandler(inputHandler, device, IO.INPUT)

reactor.run() # 이벤트를 처리하고 적절한 처리기를 호출하는 리액터를 실행합니다.

5. 1. 블로킹, 동기

python

device = IO.open()

data = device.read() # 장치에 데이터가 있을 때까지 스레드가 차단됩니다.

print(data)

```

이 방식은 I/O 장치에 데이터가 도착할 때까지 스레드(프로그램의 실행 흐름)가 차단(block)되어 다른 작업을 수행하지 못하고 대기하는 방식이다. 데이터가 도착하면 `device.read()` 함수가 데이터를 반환하고, `print(data)` 문이 실행되어 데이터를 출력한다. 이 방식은 코드가 간결하고 이해하기 쉽지만, I/O 작업이 완료될 때까지 다른 작업을 수행할 수 없다는 단점이 있다.

5. 2. 블로킹 및 논블로킹, 동기


  • 블로킹, 동기: I/O 장치에 데이터가 있을 때까지 스레드가 차단되는 방식이다.
  • 예시:

```python

device = IO.open()

data = device.read() # 장치에 데이터가 있을 때까지 스레드가 차단된다.

print(data)

```

  • 블로킹 및 논블로킹, 동기: `IO.poll()`은 최대 5초 동안 블로킹될 수 있지만, `device.read()`는 그렇지 않다.
  • 예시:

```python

device = IO.open()

ready = False

while not ready:

print("읽을 데이터가 없습니다!")

ready = IO.poll(device, IO.INPUT, 5) # 5초가 경과하거나 읽을 데이터가 있는 경우(INPUT) 제어를 반환한다.

data = device.read()

print(data)

```

  • 논블로킹, 비동기: Async/await를 사용하거나, Reactor 패턴을 사용하는 예시이다.
  • 예시 (Async/await):

```python

ios = IO.IOService()

device = IO.open(ios)

async def task():

try:

data = await device.readSome()

print(data)

except Exception:

pass

ios.addTask(task)

ios.loop() # 모든 작업이 완료될 때까지 대기하고 모든 적절한 처리기를 호출한다.

```

  • 예시 (Reactor 패턴):

```python

device = IO.open()

reactor = IO.Reactor()

def inputHandler(data):

"입력 데이터 처리기"

print(data)

reactor.stop()

reactor.addHandler(inputHandler, device, IO.INPUT)

reactor.run() # 이벤트를 처리하고 적절한 처리기를 호출하는 리액터를 실행한다.

```

비동기 I/O는 논블로킹 I/O와 매우 자주 혼동되지만, 동기 또는 비동기, 블로킹 또는 논블로킹이라는 분류는 반드시 일치하지 않는다. POSIX 환경에서 `O_NONBLOCK`이 설정된 파일 디스크립터에 대해 일반적인 read나 write를 수행하면 논블로킹이 되지만, "블록될 것 같으면 에러로 한다"는 동작이므로, 비동기가 되는 것은 아니다 (대부분의 I/O 작업은 OS 내의 버퍼 등에 의해, 동기형 API에서도 블록되지 않고 완료될 수 있는 경우도 많다).

비동기 I/O는 다음과 같은 스타일의 입출력 API에 의한 I/O이다.

# 버퍼의 내용이 커널 등에 의해 복사되거나, 또는 프로그래머의 책임으로 처리가 완료될 때까지 요구하는 프로세스가 그것을 유지해야 한다.

# (권한 위반 등, 즉시 커널이 에러 등으로 할 수 있는 경우를 제외하고) 입출력의 성패도, 입출력을 요구하는 시스템 콜의 결과로는 얻을 수 없으며, 콜백 또는 다른 시스템 콜 등으로 다시 얻을 필요가 있다.

# 이상과 같은 제한 하에, 입출력 요구의 시스템 콜은 블록되지 않고, 최소한의 처리로 즉시 종료된다.

이러한 비동기 I/O가 이용되는 경우는 "시간 제약이 엄격한 RTOS이기 때문에"와 같은 이유뿐만 아니라, 상호 배제 제어의 사정 등으로 블록시킬 수 없거나, 성능상의 이유로 엔터프라이즈 용도로 요구되는 경우, 또는 이벤트 드리븐 형 프레임워크이기 때문에 필요한 경우 등 다양하다. 별도의 스레드를 사용하여, 프로세스 내에서 비동기 I/O처럼 보이게 하는 라이브러리(프레임워크) 등도 있을 수 있다.

5. 3. 논블로킹, 비동기

논블로킹, 비동기 방식은 입출력(I/O) 작업이 완료될 때까지 기다리지 않고 즉시 제어를 반환하는 방식이다. 이 방식은 다음과 같은 특징을 가진다.

  • 논블로킹 (Non-blocking): 입출력 호출은 즉시 반환되며, 작업 완료 여부와 관계없이 다음 코드를 실행할 수 있다.
  • 비동기 (Asynchronous): 입출력 작업은 백그라운드에서 실행되며, 완료되면 콜백 함수나 다른 메커니즘을 통해 결과를 통지받는다.


다음은 논블로킹, 비동기 I/O를 구현하는 예시이다.

```python

ios = IO.IOService()

device = IO.open(ios)

def inputHandler(data, err):

"입력 데이터 처리기"

if not err:

print(data)

device.readSome(inputHandler)

ios.loop() # 모든 작업이 완료될 때까지 대기하고 모든 적절한 처리기를 호출합니다.

```

위 코드에서 `device.readSome()` 함수는 논블로킹 방식으로 데이터를 읽는다. 데이터가 준비되지 않아도 즉시 반환되며, 데이터가 준비되면 `inputHandler`라는 콜백 함수가 호출되어 결과를 처리한다. `ios.loop()`는 모든 비동기 작업이 완료될 때까지 대기하는 역할을 한다.

Async/await를 사용하면 동일한 예시를 다음과 같이 작성할 수 있다.

```python

ios = IO.IOService()

device = IO.open(ios)

async def task():

try:

data = await device.readSome()

print(data)

except Exception:

pass

ios.addTask(task)

ios.loop() # 모든 작업이 완료될 때까지 대기하고 모든 적절한 처리기를 호출합니다.

```

Reactor 패턴을 사용한 예시는 다음과 같다.

```python

device = IO.open()

reactor = IO.Reactor()

def inputHandler(data):

"입력 데이터 처리기"

print(data)

reactor.stop()

reactor.addHandler(inputHandler, device, IO.INPUT)

reactor.run() # 이벤트를 처리하고 적절한 처리기를 호출하는 리액터를 실행합니다.

```

비동기 I/O는 논블로킹 I/O와 혼동되기도 하지만, 엄밀히 말하면 동기/비동기와 블로킹/논블로킹은 반드시 일치하는 개념은 아니다. POSIX 환경에서 `O_NONBLOCK` 플래그를 설정하여 파일을 읽거나 쓰는 것은 논블로킹이지만, "블록될 것 같으면 에러로 처리"하는 동작이므로 비동기는 아니다.

비동기 I/O는 다음과 같은 특징을 갖는 입출력 API를 사용한다.

1. 프로세스는 버퍼의 내용이 커널 등에 의해 복사되거나, 프로그래머의 책임으로 처리가 완료될 때까지 버퍼를 유지해야 한다.

2. 입출력의 성공 여부는 입출력을 요구하는 시스템 콜의 결과로 즉시 얻을 수 없으며, 콜백 또는 다른 시스템 콜을 통해 확인해야 한다.

3. 입출력 요구 시스템 콜은 블록되지 않고 최소한의 처리만으로 즉시 종료된다.

비동기 I/O는 "시간 제약이 엄격한 RTOS이기 때문에" 사용되는 것은 아니다. 상호 배제 제어 문제로 블록될 수 없거나, 성능상의 이유로 엔터프라이즈 환경에서 요구되거나, 이벤트 드리븐형 프레임워크에서 필요하기 때문에 사용될 수 있다. 별도의 스레드를 사용하여 프로세스 내에서 비동기 I/O처럼 동작하는 라이브러리도 존재한다.

5. 4. Async/Await (Python)

다음은 Async/await를 사용한 파이썬 예시이다.

```python

ios = IO.IOService()

device = IO.open(ios)

async def task():

try:

data = await device.readSome()

print(data)

except Exception:

pass

ios.addTask(task)

ios.loop() # 모든 작업이 완료될 때까지 대기하고 모든 적절한 처리기를 호출합니다.

```

위 코드는 I/O를 읽는 세 가지 접근 방식 중 논블로킹, 비동기 방식을 보여준다. 객체와 함수는 추상적이다.[1]

다음은 Reactor 패턴을 사용한 예시이다.

```python

device = IO.open()

reactor = IO.Reactor()

def inputHandler(data):

"입력 데이터 처리기"

print(data)

reactor.stop()

reactor.addHandler(inputHandler, device, IO.INPUT)

reactor.run() # 이벤트를 처리하고 적절한 처리기를 호출하는 리액터를 실행합니다.

```[1]

6. 리눅스에서의 샘플 프로그램

c

/* 비동기 I/O를 사용한 파일 출력 예시 */

#include

#include

#include

#include

#include

#include

#include

#define DATA_BUF_SIZE 4096

#define DATA_BUF_NUM 128

int main(void)

{

int fd;

int n, status;

unsigned char *Aio_buff[DATA_BUF_NUM];

struct aiocb Aiocb[DATA_BUF_NUM];

struct aiocb *List[DATA_BUF_NUM];

if ((fd = open("datafile", (O_CREAT | O_WRONLY), 0666)) < 0)

{

exit(1);

}

/* 링크 리스트 생성 */

for (n = 0; n < DATA_BUF_NUM; n++)

{

Aio_buff[n] = (unsigned char*)memalign(sysconf(_SC_PAGESIZE), DATA_BUF_SIZE);

memset((void *)(Aio_buff[n]), n, DATA_BUF_SIZE); /* 데이터는 페이지 단위로 n으로 채움 */

Aiocb[n].aio_buf = Aio_buff[n];

Aiocb[n].aio_offset = (long long)(DATA_BUF_SIZE * n);

Aiocb[n].aio_nbytes = (long long)DATA_BUF_SIZE;

Aiocb[n].aio_fildes = fd;

Aiocb[n].aio_reqprio = 0;

Aiocb[n].aio_lio_opcode = LIO_WRITE;

Aiocb[n].aio_sigevent.sigev_notify = SIGEV_NONE;

List[n] = &Aiocb[n];

}

/* 비동기 I/O 발행 */

lio_listio(LIO_WAIT, (struct aiocb **)&List[0], DATA_BUF_NUM, &sig);

/**/

/* 이 사이에 파일 출력과 병행하여 다른 처리를 기술할 수 있다. */

/**/

/* 비동기 I/O 종료 대기 */

aio_suspend((const struct aiocb **)&List[0], DATA_BUF_NUM, &timeout);

/* 에러 상태 확인 */

for (n = 0; n < DATA_BUF_NUM; n++)

{

status = aio_error(&(Aiocb[n]));

if (status) printf("%d is error %d:%s\n", n, status, strerror(status));

}

/* 종료 상태 확인 */

for (n = 0; n < DATA_BUF_NUM; n++)

{

status = aio_return(&(Aiocb[n]));

if (status != DATA_BUF_SIZE) printf("%d is write error\n", n);

}

close(fd);

return 0;

}

참조

[1] 웹사이트 Ringing in a new asynchronous I/O API https://lwn.net/Arti[...] 2020-07-27
[2] 웹사이트 Registered Input-Output (RIO) API Extensions https://technet.micr[...] 2016-08-31
[3] 웹사이트 非同期プログラミング - C# | Microsoft Docs https://docs.microso[...]
[4] 웹사이트 非同期プログラミングの一般的概念 - ウェブ開発を学ぶ | MDN https://developer.mo[...]
[5] 문서 Windowsランタイム環境では、タスク完了後の実行コンテキストの自動復帰がサポートされるなど、PPLはTPLに近い動作仕様となる。
[6] 문서 Visual C++ 2015で実験的に実装されていたawait構文が、C++標準に取り込まれた。



본 사이트는 AI가 위키백과와 뉴스 기사,정부 간행물,학술 논문등을 바탕으로 정보를 가공하여 제공하는 백과사전형 서비스입니다.
모든 문서는 AI에 의해 자동 생성되며, CC BY-SA 4.0 라이선스에 따라 이용할 수 있습니다.
하지만, 위키백과나 뉴스 기사 자체에 오류, 부정확한 정보, 또는 가짜 뉴스가 포함될 수 있으며, AI는 이러한 내용을 완벽하게 걸러내지 못할 수 있습니다.
따라서 제공되는 정보에 일부 오류나 편향이 있을 수 있으므로, 중요한 정보는 반드시 다른 출처를 통해 교차 검증하시기 바랍니다.

문의하기 : help@durumis.com